
#include <apr-1/apr_file_io.h>
#include "../net/Broker4.h"
#include "../crypt/RSAKeyPair.h"
#include "../utils/Hash.h"
#include "../utils/StringUtils.h"
#include "../utils/SerialBlockSource.h"
#include "XMLDescriptor.h"

/** Initialise a new XML descriptor class, with an internal descriptor
 * document. */
XMLDescriptor::XMLDescriptor()
{
   domd = new DOMDocument();
}

/** Destroy this XML descriptor class (should make sure to save anything
 * required first). */
XMLDescriptor::~XMLDescriptor()
{
   if (domd) delete domd;
}

/** Read an encrypted XML descriptor file (to be deprecated). */
int XMLDescriptor::readEncrypted(KeyDB *keydb, const char *keyname, const char *filename, apr_pool_t *mp)
{
   apr_status_t status;
   apr_file_t *newFP;
   status = apr_file_open(&newFP, filename, APR_FOPEN_READ|APR_FOPEN_BINARY|APR_FOPEN_BUFFERED, APR_FPROT_OS_DEFAULT, mp);
   if (status != APR_SUCCESS) { CheckAPRError(status); return 0; }
   apr_finfo_t finfo;
#ifdef USE_STAT_FILE_SIZE
   status = apr_file_info_get(&finfo, APR_FINFO_SIZE, newFP);
   CheckAPRError(status);
#else
   apr_off_t offset = 0;
   status = apr_file_seek(newFP, APR_END, &offset);
   CheckAPRError(status);
   finfo.size = offset;
   offset = 0;
   status = apr_file_seek(newFP, APR_SET, &offset);
   CheckAPRError(status);
#endif
   if (finfo.size == 0) { errprint("tried to load zero length file"); return 0; }
   apr_size_t fullread = finfo.size;
   char *encd = (char *)malloc(sizeof(char)*(finfo.size));
   status = apr_file_read_full(newFP, encd, finfo.size, &fullread);
   CheckAPRError(status);
   if (fullread != finfo.size)
   {
      errprint("expected to read %d bytes, actually read %d", finfo.size, fullread);
      free(encd);
      return 0;
   }
   status = apr_file_close(newFP);
   CheckAPRError(status);
   KeyPair *kp = keydb->GetPrivateKeyByName(keyname);
   int trustdata = 0;
   std::vector<char> *decdata = Broker4::SimpleCryptDataChunk(keyname, NULL, (RSAKeyPair *)kp, NULL, encd, fullread, 1, 0, mp, &trustdata);
   free(encd);
   if ((decdata == NULL) || (decdata->size() == 0))
   {
      errprint("decryption of data failed from XML descriptor");
      if (decdata) delete decdata;
      if (kp) delete kp;
      return 0;
   }
   domd->readIn(&(decdata->at(0)), decdata->size());
   if (kp) delete kp;
   delete decdata;
}

/** Write an encrypted XML descriptor file (to be deprecated). */
int XMLDescriptor::writeEncrypted(KeyDB *keydb, const char *keyname, const char *filename, apr_pool_t *mp)
{
   KeyPair *kp = keydb->GetPublicKeyByName(keyname);
   int trustdata = 0;
   xmlBufferPtr xbp = xmlBufferCreate();
   domd->writeOut(xbp);
   std::vector<char> *encdata = Broker4::SimpleCryptDataChunk(keyname, NULL, (RSAKeyPair *)kp, NULL, (char *)xmlBufferContent(xbp), xmlBufferLength(xbp), 0, 0, mp, &trustdata);
   if (kp) delete kp;
   xmlBufferFree(xbp);
   if ((encdata == NULL) || (encdata->size() == 0))
   {
      errprint("encryption of data failed for writing XML descriptor");
      if (encdata) delete encdata;
      return 0;
   }
   apr_status_t status;
   apr_file_t *newFP;
   status = apr_file_open(&newFP, filename, APR_FOPEN_TRUNCATE|APR_FOPEN_CREATE|APR_FOPEN_WRITE|APR_FOPEN_BINARY|APR_FOPEN_BUFFERED, APR_OS_DEFAULT, mp);
   if (status == 0)
   {
      apr_size_t cesize = encdata->size();
      status = apr_file_write_full(newFP, &(encdata->at(0)), cesize, &cesize);
      CheckAPRError(status);
      status = apr_file_close(newFP);
      CheckAPRError(status);
      int retval = (cesize == encdata->size());
      delete encdata;
      return retval;
   } else
   {
      errprint("problem opening file '%s'", filename);
      delete encdata;
      return 0;
   }
}

/** Read a plain text XML descriptor from a file.
 * 
 * @param filename
 * File name of the plain text XML descriptor file to read.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int XMLDescriptor::readPlain(const char *filename)
{
   return domd->readIn(filename);
}

/** Read a plain text XML descriptor from a buffer.
 * 
 * @param buffer
 * The buffer which contains the text XML data.
 * 
 * @param length
 * The length of the text data in the buffer.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int XMLDescriptor::readPlain(const char *buffer, int length)
{
   return domd->readIn(buffer, length);
}

/** Write a plain text XML descriptor to a file.
 * 
 * @param filename
 * File name of the plain text XML descriptor file to write.
 * 
 * @return
 * Zero on success, non-zero on failure. */
int XMLDescriptor::writePlain(const char *filename)
{
   return domd->writeOut(filename);
}

/** Write a plain text XML descriptor to a string. The caller assumes responsibility for
 * freeing the memory associated with the returned string.
 * 
 * @return
 * A new string containing the XML data. */
char *XMLDescriptor::writePlain()
{
   xmlBufferPtr xbp = xmlBufferCreate();
   domd->writeOut(xbp);
   char *newstr = (char *)malloc(sizeof(char)*(xmlBufferLength(xbp)+1));
   memcpy(newstr, (char *)xmlBufferContent(xbp), sizeof(char)*(xmlBufferLength(xbp)));
   newstr[xmlBufferLength(xbp)] = '\0';
   xmlBufferFree(xbp);
   return newstr;
}

/** Returns the capabilities string as defined in the descriptor XML. The
 * caller assumes responsibility for freeing the memory associated with
 * the returned string.
 * 
 * @return
 * The string representing the capabilities required by the work. */
char *XMLDescriptor::getCapabilitiesString()
{
   xmlNodePtr capab = domd->findNode(domd->getRootNode(), "capabilities");
   return domd->getAttribValueString(capab, "capdata");
}

/** Get the XML node at the root of the block transfer tree.
 * 
 * @return
 * A pointer to the root node of the block transfer tree. */
xmlNodePtr XMLDescriptor::getBlockTransferNode()
{
   xmlNodePtr btrans = domd->findNode(domd->getRootNode(), "blocktransfer");
   if (btrans == NULL)
   {
      errprint("could not find 'blocktransfer' node");
      return NULL;
   }
   return btrans;
}

/** Get the number of blocks specified by the transfer tree.
 * 
 * @return
 * The number of blocks in the block transfer tree. */
apr_off_t XMLDescriptor::getNumberOfBlocks()
{
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) return -1;
   char *blocks = domd->getAttribValueString(btrans, "blocks");
   if (blocks == NULL)
   {
      errprint("could not find 'blocks' block count node");
      return -1;
   }
   apr_off_t retval = -1;
   sscanf(blocks, "%" APR_OFF_T_FMT, &retval);
   free(blocks);
   return retval;
}

/** Get the uncompressed size of the blocks specified by the transfer tree.
 * 
 * @return
 * Number of bytes contained in each uncompressed block. */
apr_off_t XMLDescriptor::getUncompressedBlockSize()
{
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) return -1;
   char *fblocksze = domd->getAttribValueString(btrans, "fblocksize");
   if (fblocksze == NULL)
   {
      errprint("could not find 'fblocksize' the uncompressed block size node");
      return -1;
   }
   apr_off_t retval = -1;
   sscanf(fblocksze, "%" APR_OFF_T_FMT, &retval);
   free(fblocksze);
   return retval;
}

/** Get the root node pointer to the XML describing a block.
 * 
 * @param block
 * Block number whose pointer needs to be obtained.
 * 
 * @return
 * The node pointer to the tree of this block. */
xmlNodePtr XMLDescriptor::getBlockNode(int block)
{
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) return NULL;
   xmlNodePtr blockchild = btrans->xmlChildrenNode;
   while (blockchild != NULL)
   {
      if (xmlStrcmp(blockchild->name, BAD_CAST "block") == 0)
      {
         char *foid = domd->getAttribValueString(blockchild, "oid");
         if (foid != NULL)
         {
            if (atoi(foid) == block)
            {
               free(foid);
               return blockchild;
            }
            free(foid);
         } else errprint("xml descriptor block has no id number");
      } else errprint("xml descriptor child node is not block");
      blockchild = blockchild->next;
   }
   return NULL;
}

/** Get the compressed and encrypted size of this block.
 * 
 * @param block
 * Block number whose new size needs to be obtained.
 * 
 * @return
 * The number of bytes taken up in the compressed and encrypted form. */
apr_off_t XMLDescriptor::getCompressedEncryptedSize(int block)
{
   xmlNodePtr blocknode = getBlockNode(block);
   char *t_cesize = domd->getAttribValueString(blocknode, "cesize");
   if (t_cesize == NULL)
   {
      errprint("could not find compressed and encrypted size node (cesize)");
      return -1;
   }
   apr_off_t cesize = -1;
   sscanf(t_cesize, "%" APR_OFF_T_FMT, &cesize);
   free(t_cesize);
   return cesize;
}

/** Calculate the global hash of all the blocks.
 * 
 * @param numberofblocks
 * The total number of blocks.
 * 
 * @return
 * The hexadecimal hash of all the blocks. */
const char *XMLDescriptor::calculateXORBlockHash(int numberofblocks)
{
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) return NULL;
   xmlNodePtr blockchild = btrans->xmlChildrenNode;
   char **blocklist = (char **)malloc(sizeof(char *)*(numberofblocks+1));
   for (int i = 0; i < numberofblocks; i++)
   {
      blocklist[i] = NULL;
      if (xmlStrcmp(blockchild->name, BAD_CAST "block") == 0)
      {
         if (xmlStrcmp(blockchild->xmlChildrenNode->name, BAD_CAST "sha1hash") == 0)
         {
            if (domd->hasAttrib(blockchild->xmlChildrenNode, "val"))
            {
               blocklist[i] = domd->getAttribValueString(blockchild->xmlChildrenNode, "val");
            } else errprint("expected 'val' attribute");
         } else errprint("expected 'sha1hash' node");
      } else errprint("expected 'block' node");
      blockchild = blockchild->next;
   }
   blocklist[numberofblocks] = NULL;
   char *finalhash = Hash::mergeBlockHashes((const char **)blocklist);
   for (int i = 0; i < numberofblocks; i++)
      if (blocklist[i]) free(blocklist[i]);
   free(blocklist);
   return finalhash;
}

/** Try to allocate the blocks required to store this work on the local
 * file system.
 * 
 * @param dbs
 * A pointer to the collection of databases.
 * 
 * @param store_uuid
 * The UUID of the work store associated with this allocation.
 * 
 * @param username
 * The user responsible for these allocated files.
 * 
 * @param mustreverse
 * A flag set when an error has occurred requiring the filesystem changes
 * to be rolled back.
 * 
 * @param errmsg
 * A flag that can be set with a type of error that has occurred.
 * 
 * @param mp
 * The memory pool to be used for this process.
 * 
 * @param sizeused
 * How much of the users quota has been used in this operation.
 * 
 * @return
 * Zero on success, non-zero on unrecoverable error. */
int XMLDescriptor::allocateFileSystemBlocks(AllDBs *dbs, const char *store_uuid, const char *username, int *mustreverse, int *errmsg, apr_pool_t *mp, apr_off_t *sizeused)
{
   apr_off_t extrasize = 0;
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) return -1;
   xmlNodePtr blockchild = btrans->xmlChildrenNode;
   while (blockchild != NULL)
   {
      if (xmlStrcmp(blockchild->name, BAD_CAST "block") == 0)
         if ((domd->hasAttrib(blockchild, "oid")) && (domd->hasAttrib(blockchild, "cesize")))
         {
            // t_oid and t_cesize cannot be NULL
            char *t_oid = domd->getAttribValueString(blockchild, "oid");
            char *t_cesize = domd->getAttribValueString(blockchild, "cesize");
            int oid = atoi(t_oid);
            int cesize = atoi(t_cesize);
            char *tuser = dbs->actorsdb->LookupUltimateUser(username);
            if (cesize > dbs->actorsdb->GetUserQuota(tuser))
            {
               errprint("User '%s' ran out of quota (bytes needed %" APR_OFF_T_FMT " but quota was %" APR_OFF_T_FMT ")", tuser, cesize, dbs->actorsdb->GetUserQuota(tuser));
               if (mustreverse) *mustreverse = 1;
               if (errmsg) *errmsg = PROCESSXMLDESCRIPTOR_OUTOFQUOTA;
            }
            free(t_oid);
            free(t_cesize);
            char *filename = StringUtils::varprintf("_%s.c%d", store_uuid, oid);
            Broker4::WriteFakeFile(filename, cesize, errmsg, mustreverse, mp);
            apr_size_t realsize = Broker4::GetFileSize(filename, mp);
            dbs->actorsdb->ModifyUserQuota(tuser, -((apr_off_t)(realsize)));
            extrasize += ((apr_off_t)(realsize));
            free(tuser);
            free(filename);
         }
      blockchild = blockchild->next;
   }
   if (sizeused) *sizeused = extrasize;
   return 0;
}

/** Do error checks, and then try to allocate the blocks required to store
 * this work on the local file system.
 * 
 * @param dbs
 * A pointer to the collection of databases.
 * 
 * @param store_uuid
 * The UUID of the work store associated with this allocation.
 * 
 * @param username
 * The user responsible for these allocated files.
 * 
 * @param errmsg
 * A flag that can be set with a type of error that has occurred.
 * 
 * @param origination
 * The originator for the descriptor, either a work store or a user depending
 * on whether this descriptor is for work to do or results to convey.
 * 
 * @param mp
 * The memory pool to be used for this process.
 * 
 * @param sizeused
 * How much of the users quota has been used in this operation.
 * 
 * @return
 * Zero on success, non-zero on unrecoverable error. */
int XMLDescriptor::allocateXMLDescribed(AllDBs *dbs, const char *store_uuid, const char *username, int *errmsg, const char *origination, apr_pool_t *mp, apr_off_t *sizeused)
{
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) { if (errmsg) *errmsg = PROCESSXMLDESCRIPTOR_BADXML; return -1; }
   int noofblocks = getNumberOfBlocks();
   if (noofblocks == -1) { if (errmsg) *errmsg = PROCESSXMLDESCRIPTOR_BADXML; return -1; }
   apr_off_t unblocksize = getUncompressedBlockSize();
   if (unblocksize == -1) { if (errmsg) *errmsg = PROCESSXMLDESCRIPTOR_BADXML; return -1; }
   const char *finalhash = calculateXORBlockHash(noofblocks);
   char *tuser = dbs->actorsdb->LookupUltimateUser(username);
   if (tuser == NULL)
   {
      errprint("work block does not have an ultimate owner?");
   } else
   {
      /*
      std::vector<char *> *xorbllist = dbs->actorsdb->SQLScanAllTextf("select xorblockhash from work_stores where xorblockhash='%s' and owner='%s';", finalhash, tuser);
      */
      free(tuser);
      /*
      if ((xorbllist) && (xorbllist->size() > 0))
      {
         free((void *)finalhash);
         StringUtils::destroyStringList(xorbllist);
         if (errmsg) *errmsg = PROCESSXMLDESCRIPTOR_DUPLICATE;
         errprint("work blocked from session creation as duplicate");
         return -1;
      }
      if (xorbllist) delete xorbllist;
      */
   }
   int mustcancel = 0;
   if (errmsg) *errmsg = PROCESSXMLDESCRIPTOR_SUCCESS;
   dbs->actorsdb->AddWorkStore(store_uuid, username, noofblocks, unblocksize, finalhash, origination);
   free((void *)finalhash);
   allocateFileSystemBlocks(dbs, store_uuid, username, &mustcancel, errmsg, mp, sizeused);
   if (!mustcancel) return 0;
   DBArbitrator::DeleteFileStoreByUUID(dbs, store_uuid, mp);
   if (errmsg) if (*errmsg == PROCESSXMLDESCRIPTOR_SUCCESS) *errmsg = PROCESSXMLDESCRIPTOR_GENERICERROR;
   return -1;
}

/** Generate an XML block transfer tree for this work. This involves testing
 * and hashing blocks coming from the directory serialisation and encryption
 * code to generate a manifest for the blocks.
 * 
 * @param cancel
 * As this is a long process, a cancel flag is provided to enable an
 * asynchronous cancellation of this processes from another thread.
 * 
 * @param cname
 * ???
 * 
 * @param trialkey
 * ???
 * 
 * @param directorypath
 * The directory to generate a block transfer tree for.
 * 
 * @param blocksize
 * The size of the plain text blocks.
 * 
 * @param mp
 * The memory pool to use for this process.
 * 
 * @param allblocks
 * A place to store the total number of blocks needed to send the directory.
 * 
 * @return
 * ??? */
int XMLDescriptor::generateXMLTreeForBlockTransfer(int *cancel, const char *cname, KeyPair *trialkey, const char *directorypath, apr_size_t blocksize, apr_pool_t *mp, int *allblocks)
{
   apr_status_t status;
   xmlNodePtr tblocktrans = domd->createChildNode(domd->getRootNode(), "blocktransfer");
   char *fblock = StringUtils::varprintf("%" APR_SIZE_T_FMT, blocksize);
   domd->setProperty(tblocktrans, "fblocksize", fblock);
   free(fblock);

   SerialBlockSource *sbs = new SerialBlockSource(directorypath, mp);
   
   apr_size_t totalsize = 0;
   int blockno = 0;
   int asize = blocksize;
   while (asize == blocksize)
   {
      asize = blocksize;
      int cesize = 0;
      char *hash = NULL;
      char *cemem_block = sbs->GetNextBlockEncrypted(cname, trialkey, &asize, &cesize, &hash);
      xmlNodePtr tblock = domd->createChildNode(tblocktrans, "block");
      // This scheme will flop if the workpacket >2Pb! 1Mb * 2 billion, have to resize blocks
      char *buf = NULL;
      buf = StringUtils::varprintf("%d", blockno);
      domd->setProperty(tblock, "oid", buf); free(buf);
      buf = StringUtils::varprintf("%d", asize);
      domd->setProperty(tblock, "tsize", buf); free(buf);
      buf = StringUtils::varprintf("%d", cesize);
      domd->setProperty(tblock, "cesize", buf); free(buf);
      xmlNodePtr xhash = domd->createChildNode(tblock, "sha1hash");
      domd->setProperty(xhash, "val", hash);
      printf("BLOCK %d - size: %d hash: %s compressed: %d ratio: %2.2f\n", blockno, asize, hash, cesize, (double)asize / (double)cesize);
      free(hash);
      free(cemem_block);
      blockno++;
      totalsize += asize;
      if (allblocks) *allblocks = blockno;
      if (cancel)
         if (*cancel)
            break;
   }
   char *bbuf = NULL;
   bbuf = StringUtils::varprintf("%d", blockno);
   domd->setProperty(tblocktrans, "blocks", bbuf); free(bbuf);
   bbuf = StringUtils::varprintf("%" APR_SIZE_T_FMT, totalsize);
   domd->setProperty(tblocktrans, "fsize", bbuf); free(bbuf);
   delete sbs;
}

/** Delete the block transfer tree.
 * 
 * @return
 * Zero on success, -1 there was no block transfer tree to delete. */
int XMLDescriptor::unlinkXMLTreeForBlockTransfer()
{
   xmlNodePtr btrans = getBlockTransferNode();
   if (btrans == NULL) return -1;
   xmlUnlinkNode(btrans); xmlFreeNode(btrans);
   return 0;
}

/** Generate an initial descriptor for the piece of work.
 * 
 * @param workname
 * Name for the work.
 * 
 * @param examplechain
 * The chain of examples to be used for operation checking (not implemented!).
 * 
 * @param caps
 * Capabilities required for the operation of this workpacket.
 * 
 * @param secondsmax
 * Maximum time required for the computation on this workpacket
 * (when examples are implemented, this should be a normalised time). */
void XMLDescriptor::generateInitialDescriptor(const char *workname, const char *examplechain, const char *caps, int secondsmax)
{
   domd->generateRootNode("workpacket");
   xmlNodePtr troot = domd->getRootNode();
   if ((workname == NULL) || (strlen(workname) == 0))
   {
      errprint("workname not given, defaulting to 'untitled work'");
      domd->setProperty(troot, "localname", "untitled work");
   } else
   {
      domd->setProperty(troot, "localname", workname);
   }
   char timebuffer[64+9];
   if (secondsmax < 300)
   {
      errprint("seconds given is less than five minutes, rounding up to five minutes");
      sprintf(timebuffer, "00:05:00");
   } else
   {
      sprintf(timebuffer, "%02d:%02d:%02d", ((secondsmax / 60) / 24), (secondsmax / 60) - (((secondsmax / 60) / 24) * 24), secondsmax % 60);
   }
   domd->setProperty(troot, "maxtime", timebuffer);
   xmlNodePtr tcapsnode = domd->createChildNode(troot, "capabilities");
   if ((examplechain == NULL) || (strlen(examplechain) == 0))
   {
      errprint("examplechain empty, defaulting to 'unknown'");
      domd->setProperty(tcapsnode, "examplechain", "unknown");
   } else
   {
      domd->setProperty(tcapsnode, "examplechain", examplechain);
   }
   if ((caps == NULL) || (strlen(caps) == 0))
   {
      errprint("capabilities have not been specified, defaulting to most basic 'single' required");
      domd->setProperty(tcapsnode, "capdata", "single");
   } else
   {
      domd->setProperty(tcapsnode, "capdata", caps);
   }
}
